package org.jiangy.authentication.config;

import lombok.extern.slf4j.Slf4j;
import org.jiangy.authentication.filter.OpaqueTokenAuthenticationFilter;
import org.jiangy.authentication.logout.RedisCacheClearingLogoutHandler;
import org.jiangy.authentication.store.DefaultJdbcUserDetailsManager;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.AuthenticationEventPublisher;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.DefaultAuthenticationEventPublisher;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.authorization.AuthorizationEventPublisher;
import org.springframework.security.authorization.SpringAuthorizationEventPublisher;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.zalando.problem.spring.web.advice.security.SecurityProblemSupport;

/**
 *
 * @author Administrator
 */
@Slf4j
@Configuration
@Import(SecurityProblemSupport.class)
@EnableWebSecurity
public class SecurityConfig {

    final SecurityProperties securityProperties;
    final SecurityProblemSupport problemSupport;
    final DefaultJdbcUserDetailsManager userDetailsManager;

    public SecurityConfig(SecurityProperties securityProperties,
                          SecurityProblemSupport problemSupport,
                          DefaultJdbcUserDetailsManager userDetailsManager) {
        this.securityProperties = securityProperties;
        this.problemSupport = problemSupport;
        this.userDetailsManager = userDetailsManager;
    }

    /**
     * 跨域设置
     * 1. http.cors(Customizer.withDefaults())
     * 2. 注册 corsConfigurationSource bean
     */
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        securityProperties.getAllowedOriginList().forEach(origin -> {
            log.info("[CORS] origin => {}", origin);
            config.addAllowedOrigin(origin);
        });
        config.setAllowCredentials(true);
        source.registerCorsConfiguration("/**", config);
        return source;
    }

    /**
     * 安全链配置
     */
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http, StringRedisTemplate redisTemplate) throws Exception {
        securityProperties.getWhiteList().forEach(item -> log.info("[Security WhiteList] item => {}", item));
        // @formatter:off
        http
                // 纯 REST API 情况下，禁用 csrf 验证
                .csrf(AbstractHttpConfigurer::disable)
                .cors(Customizer.withDefaults())
                // 纯 REST API 情况下，采用无状态，无需创建和维护 HttpSession
                // 该配置将 SecurityContextRepository 配置为使用 NullSecurityContextRepository，并且还阻止在会话中保存请求。
                .sessionManagement(session -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .userDetailsService(userDetailsManager)
                // 在默认的用户密码认证过滤器前，增加一个 JwtAuthenticationFilter，用于实现 Jwt 登录扩展。
                .addFilterBefore(new OpaqueTokenAuthenticationFilter(redisTemplate), LogoutFilter.class)
                .exceptionHandling(handler -> handler
                        .authenticationEntryPoint(problemSupport)
                        .accessDeniedHandler(problemSupport))
                .logout(logout -> {
                    // 覆盖默认的注销成功处理器行为，改为仅返回状态码（而不是重定向到 /login?logout）
                    logout.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())
                            .addLogoutHandler(new RedisCacheClearingLogoutHandler(redisTemplate));
                }).authorizeHttpRequests((authorize) -> authorize
                        .requestMatchers(securityProperties
                                .getWhiteList()
                                .toArray(new String[0]))
                        .permitAll()
                        .anyRequest()
                        .authenticated());
        // @formatter:off
        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    /**
     * 认证事件发布器
     */
    @Bean
    public AuthenticationEventPublisher authenticationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        return new DefaultAuthenticationEventPublisher(applicationEventPublisher);
    }

    /**
     * 授权事件发布器
     */
    @Bean
    @Primary
    public AuthorizationEventPublisher authorizationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        return new SpringAuthorizationEventPublisher(applicationEventPublisher);
    }


//    @Bean
//    public UserDetailsService userDetailsService() {
//        UserDetails springUser = User.withUsername("user1")
//                .password("{noop}password")
//                .roles("USER").build();
//
//        SimpleUser user = new SimpleUser();
//        user.setUsername("user");
//        user.setPassword("{noop}password");
//        user.setEnabled(true);
//        user.setAccountNonLocked(true);
//        user.setAccountNonExpired(true);
//        user.setCredentialsNonExpired(true);
//        user.setAuthorities(List.of(new DefaultGrantedAuthority("ROLE_USER")));
//
//        return new InMemoryUserDetailsManager(user, springUser);
//    }

    /**
     * 自定义身份认证
     * 使用场景：通过 REST API 对用户进行身份验证，而不是使用表单登录。
     *
     * @see <a href="https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/index.html#publish-authentication-manager-bean">Publish an AuthenticationManager bean</a>
     */
    @Bean
    public AuthenticationManager authenticationManager(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder, AuthenticationEventPublisher authenticationEventPublisher) {

        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
        authenticationProvider.setUserDetailsService(userDetailsService);
        authenticationProvider.setPasswordEncoder(passwordEncoder);

        ProviderManager providerManager = new ProviderManager(authenticationProvider);
        providerManager.setAuthenticationEventPublisher(authenticationEventPublisher);

        return providerManager;
    }
}
